18. Add Data Validation to ContentProvider

Importance of data validation

As you can see, it’s important to ensure that the pet data that the user entered is valid. In our context, sanity checking data (also referred to as data validation or input validation) means doing a quick test to make sure that the data is reasonable within your expectations, before inserting it into the database.

Once invalid data enters your database, it’s a mess to sort through the good and bad data. It makes analysis on your data difficult because you may not be able to trust the trends you see. Plus, the UI code becomes significantly more complex because it must handle all these abnormal values, instead of being able to make certain assumptions about the data.

Add sanity checks to PetProvider insert() and update() methods

In our app, the perfect place to do these sanity check is in the PetProvider, right before we make any changes to the database. Specifically, the PetProvider exposes the query(), insert(), update(), and delete() methods, right? But querying the data doesn’t make any changes to the database, so we don’t need to add any checks there. Deleting data doesn’t add new data either. However, inserting and updating data DOES add new data to the database, so we need to sanity checks in those provider methods. One fun analogy is to think of the ContentProvider as the policeman who allows or rejects data from entering the database.

As mentioned in the beginning of this lesson, this is another advantage of having a ContentProvider. Without one, we’d have to copy/paste the same data validation logic everywhere in the UI code that we need to insert or update a pet. And whenever there’s a lot of copying and pasting, there is room for errors. Chances are, a future developer may tweak the data validation code in one place, but accidentally forget to do it in the other places. Now all the logic can be centralized within the PetProvider file and if needs to be modified, we only need to change it in one place.

Check the values in the ContentValues object

We will be sanity checking each of the values in the ContentValues object that is passed in the insert() and update() methods. Since we have only implemented the PetProvider insert() method, let’s focus on doing data validation in that method. Later, when you implement the update() method, be sure to data validation there as well.

Step 1: Determine requirements for each type of data

The first step of this quiz is to write down the requirements for each value in the ContentValues bundle: name, breed, gender, and weight. For example, we don't want to allow null names in the database.

Step 2: Add checks in the code to enforce these requirements

The second step is to take each of these requirements and test for them in the PetProvider insert() method. Let me show you an example with the name attribute, which we don’t want to be null.

We can extract out an attribute from the ContentValues object based on the key name. We can use the ContentValues.getAsString(PetEntry.COLUMN_PET_NAME) to extract out the String value stored for name - which could be Tommy for example.

Assuming “values” is a ContentValues object:

String name = values.getAsString(PetEntry.COLUMN_PET_NAME);

You can use the other ContentValues methods like: getAsInteger() or getAsBoolean() depending on what data type the attribute that you’re interested in is. The full list of available ContentValues method is on the documentation page.

Then we can check if the name from the ContentValues object is null or not. If it’s null, we should throw a new IllegalArgumentException with an error message saying that the “Pet requires a name,” instead of proceeding with creating a new pet. That way, whichever developer is calling this provider method will know that they need to change their code to provide a name for the pet.

(Note: As introduced in the Android Basics: Networking course, it can be okay to throw an exception and crash the app as a signal to developers that something bad happened, and it would be even worse to continue with accepting the bad data and trying to accommodate it. Ideally, the UI code calling this method would be smart and surface an error to the end-user to tell them to provide a pet name before it gets to this point where the app crashes.)

In PetProvider.java:

private Uri insertPet(Uri uri, ContentValues values) {
    // Check that the name is not null
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
        throw new IllegalArgumentException("Pet requires a name");
    }

    … 

There are 2 reasons why the name could be null. Either the Contentvalues object was explicitly added with the code: values.put(PetEntry.COLUMN_PET_NAME, null). Or the key / value pair was never added to the ContentValues object to begin with. Remember that a ContentValues object in the Pets app is not guaranteed to have all 4 pet attributes in it. Somewhere in the UI code, we may have accidentally forgotten to add an attribute like the name, and only managed to added breed, gender, and weight to the ContentValues bundle.

Regardless of how it could have happened, the PetProvider only cares that the data does not have a null name, otherwise it will throw an error. In this quiz, you will fill in the TODO by adding more code to the insertPet() method to ensure that the ContentValues object meets the requirements you spelled out in step 1.

In PetProvider.java:

private Uri insertPet(Uri uri, ContentValues values) {
    // Check that the name is not null
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
        throw new IllegalArgumentException("Pet requires a name");
    }

    // TODO: Finish sanity checking the rest of the attributes in ContentValues

    // Get writeable database
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Insert the new pet with the given values
    long id = database.insert(PetEntry.TABLE_NAME, null, values);
    // If the ID is -1, then the insertion failed. Log an error and return null.
    if (id == -1) {
        Log.e(LOG_TAG, "Failed to insert row for " + uri);
        return null;
    }

    // Return the new URI with the ID (of the newly inserted row) appended at the end
    return ContentUris.withAppendedId(uri, id);
}

QUIZ QUESTION::

Match the data type with the requirements.

ANSWER CHOICES:



Requirements

Attributes

Weight

Name

Breed

Gender

SOLUTION:

Requirements

Attributes

Weight

Name

Breed

Gender

Solution

  • Name - Cannot be NULL
  • Breed - Can be NULL, so there is no need to check that value in the provider.
  • Gender - Cannot be NULL. Must equal one of these constants: GENDER_MALE, GENDER_FEMALE, or GENDER_UNKNOWN.
  • Weight - Technically, the weight could be null because of the way we defined our pets table. We added a database constraint to use default value 0 if no weight was provided. So we can allow a null weight value. BUT if a weight value is provided, then we must make sure it’s greater or equal to 0. No negative weights allowed.

Check for gender

For the sanity check for gender, since it is stored as an integer, we use the ContentValues getAsInt() method and pass in the gender column key.

In PetProvider insertPet() method:

  Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);

If the gender is null or it’s not one of the valid gender values, then we throw an IllegalArgumentException with the error message “Pet requires valid gender.” Note that adding the “!” symbol in front of PetEntry.isValidGender(gender) takes the opposite of that value. If isValidGender() returns true, then adding the “!” symbol in front of it, will turn that value to false. If isValidGender() returns false, then adding the “!” symbol in front of it, will turn that value to true. I also utilized the “||” operator because if either the gender is null or the gender is invalid, then the “if” check will be true, and we should throw an exception. This logic is a little tricky so try to isolate each part of the “if” check, one by one, to make sure you follow along.

  if (gender == null || !PetEntry.isValidGender(gender)) {
      throw new IllegalArgumentException("Pet requires valid gender");
  }

I defined the isValidGender() method in the PetContract’s PetEntry class where the gender constants are defined. The method takes as input an integer, and returns true or false if the integer is a valid gender (equals GENDER_MALE, GENDER_FEMALE, or GENDER_UNKNOWN). I decided to put this helper method in the PetContract because I could imagine it being used in multiple places throughout the app. To see the context of where this method was added to the PetContract file, see this link.

In PetContract.java file, PetEntry class:

  /**
   * Returns whether or not the given gender is {@link #GENDER_UNKNOWN}, {@link #GENDER_MALE},
   * or {@link #GENDER_FEMALE}.
   */
  public static boolean isValidGender(int gender) {
      if (gender == GENDER_UNKNOWN || gender == GENDER_MALE || gender == GENDER_FEMALE) {
          return true;
      }
      return false;
  }

Okay, that’s all we need to do to ensure the gender value meets our requirements.

Check for weight

To extract out the weight value from the ContentValues object, we use the ContentValues getAsInteger() method, and pass in the weight as the key for the key / value pair.

In PetProvider insertPet() method:

  // If the weight is provided, check that it's greater than or equal to 0 kg
  Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);

If the weight is null, that’s fine, and we can proceed with insertion (the database will insert default weight 0 automatically). If the weight is not null AND it’s a negative weight, then we need to throw an exception with the message “Pet requires valid weight.” We use the “&&” symbol to indicate that both “weight != null” must be true and “weight < 0” must be true, in order for the whole test condition to be true, and for the code within the “if” statement to execute.

  if (weight != null && weight < 0) {
      throw new IllegalArgumentException("Pet requires valid weight");
  }

If all sanity checks pass, and the values are reasonable, then we can proceed with inserting the pet into the database as we already had code for. This is the full insertPet() method at the end of this coding task.

In PetProvider.java:

private Uri insertPet(Uri uri, ContentValues values) {
    // Check that the name is not null
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
        throw new IllegalArgumentException("Pet requires a name");
    }

    // Check that the gender is valid
    Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
    if (gender == null || !PetEntry.isValidGender(gender)) {
        throw new IllegalArgumentException("Pet requires valid gender");
    }

    // If the weight is provided, check that it's greater than or equal to 0 kg
    Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
    if (weight != null && weight < 0) {
        throw new IllegalArgumentException("Pet requires valid weight");
    }

    // No need to check the breed, any value is valid (including null).

    // Get writeable database
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    // Insert the new pet with the given values
    long id = database.insert(PetEntry.TABLE_NAME, null, values);
    // If the ID is -1, then the insertion failed. Log an error and return null.
    if (id == -1) {
        Log.e(LOG_TAG, "Failed to insert row for " + uri);
        return null;
    }

    // Return the new URI with the ID (of the newly inserted row) appended at the end
    return ContentUris.withAppendedId(uri, id);
}

See the complete code diff here.

Nice work! Hopefully you’ve realized how adding some basic checks to your provider can ensure you have clean data in your database, and save you from a lot of headaches later on.